package classictravellersalesman;

import java.awt.geom.CubicCurve2D;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

import jcuda.Pointer;
import jcuda.Sizeof;
import jcuda.driver.CUdeviceptr;
import jcuda.driver.CUfunction;
import jcuda.driver.CUmodule;
import jcuda.driver.JCudaDriver;
import jcuda.runtime.*;
import stochastic.Solution;

public class ClassicTravellerPath  
			implements Solution {
	private Graph obj;
	private int[] path;
	private CUfunction kernel;
	private CUdeviceptr perturbSpace;
	private CUdeviceptr[] perturbSpacePtr;
	private CUdeviceptr curSolDevice;
	private CUdeviceptr deviceGraph;
	private CUdeviceptr tempDevice;
	private CUdeviceptr stageDevice;
	private CUdeviceptr flagsDevice;
	private float c;
	public ClassicTravellerPath(Graph graph) {
		obj = graph;
		path = new int[getDegreeOfFreedom()];
		/*try {
			InputStream graphFile = new FileInputStream("test/realTSP/ulysses22.opt.tour");
			Scanner sc = new Scanner(graphFile);
			for(int i=0;i<22;++i) {
				path[i] = sc.nextInt()-1;
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}*/
		for(int i=0; i<path.length;++i) {
			path[i] = i;
		}
	}
	
	public ClassicTravellerPath(ClassicTravellerPath rhs) {
		obj = rhs.obj;
		path = new int[getDegreeOfFreedom()];
		for(int i=0; i<path.length;++i) {
			path[i] = rhs.path[i];
		}
		
		perturbSpace = rhs.perturbSpace;
		perturbSpacePtr = rhs.perturbSpacePtr;
		curSolDevice = rhs.curSolDevice;
		tempDevice = rhs.tempDevice;
		kernel = rhs.kernel;
		flagsDevice = rhs.flagsDevice;
		stageDevice = rhs.stageDevice;
	}
	public double getCost() {
		double result = 0;
		for (int i = 0; i < path.length; i++) {
			result+=obj.getDistance(path[i], path[(i+1)%path.length]);			
		}
		return result;
	}
	
	public Solution perturb() {
		Random rand = new Random();
		ClassicTravellerPath next = this.clone();
		int a = rand.nextInt(getDegreeOfFreedom());
		int b = rand.nextInt(getDegreeOfFreedom());
		int temp = next.path[a];
		next.path[a] = next.path[b];
		next.path[b] = temp;
		return next;
	}
	
	public String toString() {
		StringBuffer result = new StringBuffer("[");
		for(int i=0;i<path.length;++i) {
			result.append(path[i]);
			result.append(',');
		}
		result.append(path[0]);
		result.append(']');
		return result.toString();
	}
	
	public ClassicTravellerPath clone() {
		ClassicTravellerPath result = new ClassicTravellerPath(obj);
		result.path = this.path.clone();
		return result;
	}

	public int getDegreeOfFreedom() {
		return obj.getDimension();
	}
	
	public Graph getGraph() {
		return obj;
	}
	
	public int[] getPath() {
		return path;
	}

	@Override
	public void loadCUDA(int numPerturb,int numThreads, double t) throws IOException {
		c = (float)t;
		String cubinFileName = prepareCubinFile("./src/classictravellersalesman/tspKernelExec.cu");
		
		CUmodule module = new CUmodule();
        JCudaDriver.cuModuleLoad(module, cubinFileName);
        CUfunction function = new CUfunction();
        JCudaDriver.cuModuleGetFunction(function, module, "perturbSolution");
		
		
		int size = obj.vertexCount;
		float[][] matrix  = new float[size][size];
		for(int i=0;i<size;++i) {
			for(int j=0;j<size;++j) {
				matrix[i][j] = (float)obj.matrix[i][j];
			}
		}
		
		
		
		CUdeviceptr graphDevicePtr[] = new CUdeviceptr[obj.vertexCount];
		for(int i=0;i<obj.vertexCount;++i) {
			graphDevicePtr[i] = new CUdeviceptr();
			JCudaDriver.cuMemAlloc(graphDevicePtr[i], size * Sizeof.FLOAT);
		}
		
		for(int i=0;i<obj.vertexCount;++i) {
			JCudaDriver.cuMemcpyHtoD(graphDevicePtr[i], Pointer.to(matrix[i]), size*Sizeof.FLOAT);
		}
		
		deviceGraph = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(deviceGraph, obj.vertexCount*Sizeof.POINTER);
		JCudaDriver.cuMemcpyHtoD(deviceGraph, Pointer.to(graphDevicePtr),
	            obj.vertexCount * Sizeof.POINTER);
		
		
		
		curSolDevice  = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(curSolDevice, path.length*Sizeof.INT);
		JCudaDriver.cuMemcpyHtoD(curSolDevice, Pointer.to(path), path.length*Sizeof.INT);
		
		
		perturbSpacePtr = new CUdeviceptr[numThreads];
		for(int i=0;i<numThreads;++i) {
			perturbSpacePtr[i] = new CUdeviceptr();
			JCudaDriver.cuMemAlloc(perturbSpacePtr[i], path.length* Sizeof.INT);
		}
		
		perturbSpace = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(perturbSpace, numThreads*Sizeof.POINTER);
		JCudaDriver.cuMemcpyHtoD(perturbSpace, Pointer.to(perturbSpacePtr), numThreads*Sizeof.POINTER);

		
		
		tempDevice = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(tempDevice, Sizeof.FLOAT);
		JCudaDriver.cuMemcpyHtoD(tempDevice, Pointer.to(new float[]{(float)t}), Sizeof.FLOAT);
		
		stageDevice = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(stageDevice, Sizeof.INT);
		JCudaDriver.cuMemcpyHtoD(stageDevice, Pointer.to(new int[]{0}), Sizeof.INT);
		
		
		//--------------------------
		int flagsSize = (numThreads-1)/32+1;
		flagsDevice = new CUdeviceptr();
		JCudaDriver.cuMemAlloc(flagsDevice, flagsSize*Sizeof.INT);
		
		int[] flagsHost = new int[flagsSize];
		for(int i=0;i<flagsSize;++i) {
			flagsHost[i] = 0;
		}
		JCudaDriver.cuMemcpyHtoD(flagsDevice, Pointer.to(flagsHost), Sizeof.INT*flagsSize);
		//---------------------------
		
		
			    
        JCudaDriver.cuFuncSetBlockShape(function, 20, 1, 1);
       
        
        
        Pointer graphPointer = Pointer.to(deviceGraph);
        Pointer solutionPointer = Pointer.to(curSolDevice);
        Pointer spacePointer  = Pointer.to(perturbSpace);
        Pointer perturbCntPointer = Pointer.to(new int[]{numPerturb});
        Pointer sizeSol = Pointer.to(new int[]{getDegreeOfFreedom()});
        Pointer tPointer = Pointer.to(tempDevice);
        Pointer stagePointer = Pointer.to(stageDevice);
        Pointer flagsPointer = Pointer.to(flagsDevice);
        
        int offset = 0;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, graphPointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, solutionPointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, spacePointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        offset = JCudaDriver.align(offset, Sizeof.INT);
        JCudaDriver.cuParamSetv(function, offset, perturbCntPointer, Sizeof.INT);
        offset += Sizeof.INT;
        offset = JCudaDriver.align(offset, Sizeof.INT);
        JCudaDriver.cuParamSetv(function, offset, sizeSol, Sizeof.INT);
        offset += Sizeof.INT;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, tPointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, stagePointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        offset = JCudaDriver.align(offset, Sizeof.POINTER);
        JCudaDriver.cuParamSetv(function, offset, flagsPointer, Sizeof.POINTER);
        offset += Sizeof.POINTER;
        
        JCudaDriver.cuParamSetSize(function, offset);
		
		kernel =  function;
	}
	
	@Override
	public void launchKernel() {
		JCudaDriver.cuLaunchGrid(kernel, 128, 1);
		JCudaDriver.cuCtxSynchronize();
	}
	@Override
	public Solution cudaChoiceSolution(int numThreads, int ok) {
		double stageTemp = 3;
		int[] vectorOfPerturb = new int[getDegreeOfFreedom()];
		Random r = new Random();
		int choice;
		if(c>stageTemp) { 
			 choice = r.nextInt(numThreads);
		}
		else
		{
			ArrayList<Integer>  variants = new ArrayList<Integer>();
			choice = 0;
			int flagsSize = (numThreads-1)/32+1;
			int[] flagsHost = new int[flagsSize];
			JCudaDriver.cuMemcpyDtoH(Pointer.to(flagsHost), flagsDevice, flagsSize*Sizeof.INT);
	extloop:for(int i=0;i<flagsSize;++i) {
				if(flagsHost[i]!=0) {
					choice = 32*i;
					int mask=1;//0x80000000;
					for(int j=0;j<32;++j) {
						if((flagsHost[i]&mask)!=0) {
							variants.add(choice + 31-j);
						}
						mask = mask<<1;
					}
					
				}
			}
			if(variants.size()!=0)
				choice = variants.get(r.nextInt(variants.size()));
		}
		long now = System.currentTimeMillis();
		
		
		JCudaDriver.cuMemcpyDtoH(Pointer.to(vectorOfPerturb), perturbSpacePtr[choice], getDegreeOfFreedom()*Sizeof.INT);
		//System.out.println("Copy:" + (System.currentTimeMillis()-now));
		
		ClassicTravellerPath p = new ClassicTravellerPath(this);
		for(int j=0;j<getDegreeOfFreedom();++j) {
			p.path[j] = vectorOfPerturb[j];
		}
		
		p.c = c;
		if(ok==1)
			p.c = (float)(c*0.98);
		JCudaDriver.cuMemcpyHtoD(p.curSolDevice, Pointer.to(p.path), p.path.length*Sizeof.INT);
		JCudaDriver.cuMemcpyHtoD(p.tempDevice, Pointer.to(new float[]{p.c}), Sizeof.FLOAT);
		if(p.c<stageTemp) {
			JCudaDriver.cuMemcpyHtoD(p.stageDevice, Pointer.to(new int[]{1}), Sizeof.INT);
		}
		int flagsSize = (numThreads-1)/32+1;
		int[] flagsHost = new int[flagsSize];
		for(int i=0;i<flagsSize;++i) {
			flagsHost[i] = 0;
		}
		JCudaDriver.cuMemcpyHtoD(p.flagsDevice, Pointer.to(flagsHost), Sizeof.INT*flagsSize);
		
		return p;
	}
	@Override
	public void cudaClean() {
		JCudaDriver.cuMemFree(perturbSpace);
		JCudaDriver.cuMemFree(curSolDevice);
		JCudaDriver.cuMemFree(tempDevice);
		JCudaDriver.cuMemFree(stageDevice);
		JCudaDriver.cuMemFree(flagsDevice);
		
	}
	@Override
	public void setParam() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public String getKernelFile() {
		return "./tspKernelExec.cu";
	}
	
	private static String prepareCubinFile(String cuFileName) throws IOException
    {
        int endIndex = cuFileName.lastIndexOf('.');
        if (endIndex == -1)
        {
            endIndex = cuFileName.length()-1;
        }
        String cubinFileName = cuFileName.substring(0, endIndex+1)+"cubin";
        File cubinFile = new File(cubinFileName);
        if (cubinFile.exists())
        {
            return cubinFileName;
        }
        
        File cuFile = new File(cuFileName);
        if (!cuFile.exists())
        {
            throw new IOException("Input file not found: "+cuFileName);
        }
        String modelString = "-m"+System.getProperty("sun.arch.data.model");        
        String command = 
            "nvcc " + modelString + " -arch sm_11 -cubin "+
            cuFile.getPath()+" -o "+cubinFileName;
        
        System.out.println("Executing\n"+command);
        Process process = Runtime.getRuntime().exec(command);

        String errorMessage = new String(toByteArray(process.getErrorStream()));
        String outputMessage = new String(toByteArray(process.getInputStream()));
        int exitValue = 0;
        try
        {
            exitValue = process.waitFor();
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
            throw new IOException("Interrupted while waiting for nvcc output", e);
        }

        System.out.println("nvcc process exitValue "+exitValue);
        if (exitValue != 0)
        {
            System.out.println("errorMessage:\n"+errorMessage);
            System.out.println("outputMessage:\n"+outputMessage);
            throw new IOException("Could not create .cubin file: "+errorMessage);
        }
        return cubinFileName;
    }

    /**
     * Fully reads the given InputStream and returns it as a byte array.
     *  
     * @param inputStream The input stream to read
     * @return The byte array containing the data from the input stream
     * @throws IOException If an I/O error occurs
     */
    private static byte[] toByteArray(InputStream inputStream) throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte buffer[] = new byte[8192];
        while (true)
        {
            int read = inputStream.read(buffer);
            if (read == -1)
            {
                break;
            }
            baos.write(buffer, 0, read);
        }
        return baos.toByteArray();
    }



}
